/* ----------------------------------------------------------------------------
* File: box_server.cpp
*
* Desc: Definition of CBoxServer class
*
* Created: 19/02/2011, by Alexey Korshunov
* ---------------------------------------------------------------------------- */
#include "common.h"
#include "utils.h"
#include "box_server.h"


CFileLogger* CBoxServer::m_logger = NULL;


// Cleanup function closes all opened sockets
// param - pointer to a server_env structure
void close_server_sockets(void *param)
{
	server_env *env = static_cast<server_env*>(param);
	if (env == NULL)
		return;

	if (env->listen_data_sock != -1)
	{
		shutdown(env->listen_data_sock, 2);
		close(env->listen_data_sock);
	}
	if (env->listen_control_sock != -1)
	{
		shutdown(env->listen_control_sock, 2);
		close(env->listen_control_sock);
	}

	env->logger->Print("worker thread: Stop\n");
	pthread_mutex_lock(&env->mutex_stop);
	pthread_cond_signal(&env->cv_stop);
	pthread_mutex_unlock(&env->mutex_stop);
}


// Thread function for worker thread of the server
// param - pointer to a server_env structure
void* worker_thread(void *param)
{
	int sock;
	int status;
	struct sockaddr_in clientaddr;

	// Disable cancellation of current thread
	pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);

	server_env *env = static_cast<server_env*>(param);
	if (env == NULL)
		pthread_exit(NULL);

	pthread_cleanup_push(&close_server_sockets, (void*)env);

	env->logger->Print("server: thread id = %lu\n", pthread_self());

	// Listen a socket
	env->logger->Print("server: Listen data socket...");
	if (UtilsSocketListen(env->listen_data_sock, env->server->GetParams()->max_connections, &status, env->logger->GetFileDescriptor()) != UTILS_OK)
	{
		env->logger->Print("\nERROR: Unable to listen data socket, sock - %d, %s", env->listen_data_sock, strerror(status));
		pthread_exit(NULL);
	}
	env->logger->Print("success\n");


	// Signal to main thread and enable cancellation
	pthread_cond_signal(&env->cv_start);
	pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
	pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);


	// main loop
	while(!env->stop)
	{
		memset(&clientaddr, 0, sizeof(clientaddr));

		// Accept new connection
		sock = UtilsSocketAccept(env->listen_data_sock, BOX_SELECT_TIMEOUT, &clientaddr, &status, env->logger->GetFileDescriptor());
		if (sock == UTILS_ERROR)
		{
			env->logger->Print("server: ERROR - Unable to accept connection, %s\n", strerror(status));
			pthread_exit(NULL);
		}

		// Handling new connection
		if (sock != UTILS_TIMEOUT && sock > 0)
		{
			env->logger->Print("\nserver: New connection from %s on socket %d\n", inet_ntoa(clientaddr.sin_addr), sock);
			
			// Add job in pool
			if (env->server->GetParams()->use_thread_pool)
			{
				//if (OwnThreadPoolAddJob(server->m_thread_pool, &CBoxConnection::ThreadFuncEntry, (void*)conn) != POOL_OK)
				//{
				//	server->m_logger->Print("worker thread: ERROR - Unable to add job to a thread pool\n");
				//	break;
				//}
			}
			else
			{
				// Open new connection
				CBoxConnection *conn = env->server->OpenConnection(sock, &clientaddr);
				if (conn == NULL)
					pthread_exit(NULL);
				env->logger->Print("server: Connection started\n");
			}
		}
	}

	pthread_cleanup_pop(1);
	pthread_exit(NULL);
}


CBoxServer::CBoxServer():
m_thread_id((pthread_t)0),
m_init(false)/*,
m_thread_pool(NULL)*/
{
	pthread_mutex_init(&m_mutex_open_box, NULL);

	// Create maps
	m_map_conn = new CMapConnection();
	m_map_boxes = new CMapUserBox();
}

CBoxServer::~CBoxServer()
{
	// Try to stop server
	if (!m_env.stop)
	{
		try
		{
			Stop();
		}
		catch (std::exception& ex)
		{
			m_logger->Print("server: ERROR - exception by stopping server\n");
		}
	}

	pthread_mutex_destroy(&m_mutex_open_box);

	// Destroy logger
	delete m_logger;
	
	// Destroy maps
	delete m_map_boxes;
	delete m_map_conn;
}

// Open user box
CUserBox* CBoxServer::AttachUserBox(const char *username, const CBoxConnection *conn)
{
	CUserBox *box = NULL;

	CMutexLocker lock(m_mutex_open_box);	// lock this operation

	// Try to find box object in already opened boxes
	box = m_map_boxes->FindBoxByUserName(username);
	if (box == NULL)
	{
		m_logger->Print("server: Box object not found in opened...\n");

		// Create new box object
		m_logger->Print("server: Create new box object...\n");
		box = new CUserBox(this);

		// Open box
		unsigned int id = (unsigned int)m_map_boxes->Count();
		if (box->Open(username, id) != BOX_SERVER_OK)
		{
			m_logger->Print("server: ERROR - Unable to open box for user %s\n", username);
			delete box;
			return NULL;
		}

		// Add to map
		m_map_boxes->Add(box);
	}
	else
		m_logger->Print("server: User box found as opened, id = %d\n", box->GetId());

	// Add this connection as subscriber
	box->AddSubscriber(const_cast<CBoxConnection*>(conn));

	m_logger->Print("server: Box object [%d] is attached\n", box->GetId());
	return box;
}

// Detach user box
int CBoxServer::DetachUserBox(unsigned int id_box, CBoxConnection *conn)
{
	// Try to find box
	CUserBox *box = m_map_boxes->FindBoxById(id_box);
	if (box == NULL)
	{
		m_logger->Print("server: ERROR - Unable to find related box object, id_conn = %d, id_box = %d\n", conn->GetId(), id_box);
		return BOX_SERVER_ERROR;
	}

	// Delete connection object as subscriber
	box->DeleteSubscriber(conn);

	// Close box if count of subscriber is null
	if (!box->CountSubscribers())
	{
		m_logger->Print("server: User box [%d] contains 0 subscribers, now it will be closed\n", id_box);
		box->Close();
		m_map_boxes->Delete(id_box);
	}

	const sockaddr_in *remoteaddr = conn->GetRemoteAddress();
	m_logger->Print("server: Box object [%d] is detached for connection %s\n", id_box, inet_ntoa(remoteaddr->sin_addr));
	return BOX_SERVER_OK;
}

// Find box by user name
CUserBox* CBoxServer::FindOpenedBoxByUserName(const char *username)
{
	return m_map_boxes->FindBoxByUserName(username);
}

// Get count opened boxes
size_t CBoxServer::GetCountOpenedBoxes() const
{
	return m_map_boxes->Count();
}

// Open connection
CBoxConnection* CBoxServer::OpenConnection(int sock, const struct sockaddr_in *remoteaddr)
{
	// Create connection object
	CBoxConnection *conn = new CBoxConnection(this);

	// Init object
	conn_init_info info;
	memset(&info, 0, sizeof(info));
	info.id = m_map_conn->Count();
	info.control_sock = -1;
	info.data_sock = sock;
	memcpy(&info.remote_addr, remoteaddr, sizeof(struct sockaddr_in));
	if (conn->Init(const_cast<const conn_init_info&>(info)) != BOX_SERVER_OK)
	{
		m_env.logger->Print("server: ERROR - Unable to init connection object\n");
		delete conn;
		return NULL;
	}

	// Add to map
	m_map_conn->Add(conn);

	// Start handling process
	if (conn->StartHandling() != BOX_SERVER_OK)
	{
		m_env.logger->Print("server: ERROR - Failed to start handling of connection\n");
		delete conn;
		return NULL;
	}

	return conn;
}

// Close connection
void CBoxServer::CloseConnection(unsigned int id_conn)
{
	// Find connection by identifier
	CBoxConnection *conn = m_map_conn->FindConnectionById(id_conn);
	if (conn == NULL)
	{
		m_logger->Print("server: ERROR - Unable to find connection object, id = %d\n", id_conn);
		return;
	}

	const sockaddr_in *remoteaddr = conn->GetRemoteAddress();
	m_logger->Print("server: Connection with %s is closed\n", inet_ntoa(remoteaddr->sin_addr));

	// Delete connection object from map
	m_map_conn->Delete(id_conn);
}



/************************************************************************/
/*	Desc:	Init server                                                   
	Params:	params - server parameters 									*/
/************************************************************************/
void CBoxServer::Init(box_server_params& params) throw(std::exception)
{
	int status;

	// Check params
	if (CheckParams(params) != 0)
		throw std::exception();

	// Copy parameters
	m_params = params;

	try
	{
		m_logger = new CFileLogger("./server.log");
	}
	catch(...)
	{
		throw std::exception();
	}
	
	m_env.logger = m_logger;
	m_env.server = this;

	m_env.logger->Print("----- Initializing server -----\n");

	if (m_params.control_port == 0)
		m_params.control_port = BOX_DEFAULT_CONTROL_PORT;
	if (m_params.data_port == 0)
		m_params.data_port = BOX_DEFAULT_DATA_PORT;
	if (m_params.max_connections == 0)
		m_params.max_connections = BOX_DEFAULT_MAX_CONN;

	m_env.logger->Print("Init parameters:\n");
	m_env.logger->Print("	data_port = [%d]\n", m_params.data_port);
	m_env.logger->Print("	control_port = [%d]\n", m_params.control_port);
	m_env.logger->Print("	max_connections = [%d]\n", m_params.max_connections);
	m_env.logger->Print("	use_syslog = [%d]\n", m_params.use_syslog);
	m_env.logger->Print("	use_thread_pool = [%d]\n", m_params.use_thread_pool);

	// Create control socket
	//m_logger->Print("Socket creating (control)...\n");
	//m_control_sock = tcp_socket(m_params.control_port, /*&m_logger->Print*/NULL);
	//if (m_control_sock < 0)
	//	throw std::exception();
	//m_logger->Print("success\n");

	// Create data socket
	m_env.logger->Print("Socket creating (data)...");
	m_env.listen_data_sock = UtilsTCPSocket(m_params.data_port, &status, NULL);
	if (m_env.listen_data_sock < 0)
	{
		m_env.logger->Print("ERROR: Unable to create data socket\n");
		throw std::exception();
	}
	m_env.logger->Print("success\n");

	m_init = true;
}


/************************************************************************/
/*	Desc:	Start server                                                   
	Params:																*/
/************************************************************************/
void CBoxServer::Start() throw(std::exception)
{
	struct timespec ts;

	if (!m_init)
	{
		m_env.logger->Print("ERROR: Server isn't initialized\n");
		throw std::exception();
	}

	if (!m_env.stop)
	{
		m_env.logger->Print("Server already running\n");
		throw std::exception();
	}

	m_env.logger->Print("\n----- Start server -----\n");

	// Create thread pool
	/*if (m_params.use_thread_pool)
	{
		thread_pool_set_t pool_set;
		memset(&pool_set, 0, sizeof(thread_pool_set_t));
		pool_set.min_threads = BOX_DEFAULT_MIN_THREADS;
		pool_set.max_threads = BOX_DEFAULT_MAX_THREADS;
		pool_set.time_out = BOX_POOL_TIMEOUT;
		pool_set.err_print = NULL;
		pool_set.inf_print = NULL;
		m_thread_pool = OwnThreadPoolCreate(&pool_set);
		if (m_thread_pool == NULL)
		{
			m_env.logger->Print("ERROR: Unable to create thread pool\n");
			throw std::exception();
		}
	}*/

	// Create worker thread
	m_env.logger->Print("Creating worker thread...\n");
	m_env.stop = false;
	if (UtilsThreadCreate(&m_thread_id, 0, &worker_thread, (void*)&m_env, m_env.logger->GetFileDescriptor()) != UTILS_OK)
	{
		m_env.logger->Print("ERROR: Unable to create worker thread\n");
		throw std::exception();
	}

	// Wait worker thread
	pthread_mutex_lock(&m_env.mutex_start);
	while(1)
	{
		clock_gettime(CLOCK_REALTIME, &ts);
		ts.tv_sec += BOX_START_THREAD_TIMEOUT;
		if (pthread_cond_timedwait(&m_env.cv_start, &m_env.mutex_start, &ts) != ETIMEDOUT)
			break;

		if (UtilsThreadIsExist(&m_thread_id) != UTILS_OK)	// thread doesn't exist already
			break;
	}
	pthread_mutex_unlock(&m_env.mutex_start);

	if (UtilsThreadIsExist(&m_thread_id) != UTILS_OK)
	{
		m_env.logger->Print("ERROR: Worker thread is failed\n");
		throw std::exception();
	}

	m_env.logger->Print("------ success ------\n");
}


/************************************************************************/
/*	Desc:	Stop server                                                   
	Params:																*/
/************************************************************************/
void CBoxServer::Stop() throw(std::exception)
{
	struct timespec ts;

	m_env.logger->Print("\n----- Stop server -----\n");
	if (!m_init)
	{
		m_env.logger->Print("ERROR: Server isn't initialized\n");
		throw std::exception();
	}

	if (m_env.stop)
	{
		m_env.logger->Print("Server not running\n");
		throw std::exception();
	}

	// Wait all jobs in thread pool
	/*if (m_params.use_thread_pool)
	{
		if (OwnThreadPoolWait(m_thread_pool) != POOL_OK)
		{
			m_logger->Print("ERROR: Failed while waiting of thread pool\n");
			throw std::exception();
		}
		OwnThreadPoolDestroy(m_thread_pool);
	}*/

	// Cancel worker thread
	m_env.stop = true;
	if (UtilsThreadCancel(&m_thread_id, /*&CBoxServer::LogEntry*/NULL) != UTILS_OK)
		throw std::exception();

	// Wait thread
	pthread_mutex_lock(&m_env.mutex_stop);
	while(1)
	{
		clock_gettime(CLOCK_REALTIME, &ts);
		ts.tv_sec += BOX_STOP_THREAD_TIMEOUT;
		if (pthread_cond_timedwait(&m_env.cv_stop, &m_env.mutex_stop, &ts) != ETIMEDOUT)
			break;

		if (UtilsThreadIsExist(&m_thread_id) != UTILS_OK)	// thread already doesn't exist
			break;
	}
	pthread_mutex_unlock(&m_env.mutex_stop);

	// Stop all connections
	m_map_conn->Stop();

	m_logger->Print("----- success -----\n");
}


/************************************************************************/
/*	Desc:	Check input parameters                                                   
	Params:	params - server parameters 									*/
/************************************************************************/
int CBoxServer::CheckParams(box_server_params& params)
{
	return 0;
}

// Log entry
void CBoxServer::LogEntry(const char *msg,...)
{
	char buf[1024];
	va_list argptr;

	va_start(argptr, msg);
	vsnprintf(buf, sizeof(buf), msg, argptr);
	m_logger->Print(buf);
	va_end(argptr);
}